(ns ca.uoit.molly.lucene-helper
 "Helper functions that exposes Lucene API directly to Clojure"
 
  (:import 
    (java.io File StringReader)
    (org.apache.lucene.document Document Field Field$Store Field$Index)
    (org.apache.lucene.index Term IndexReader IndexWriter IndexWriter$MaxFieldLength)
    (org.apache.lucene.queryParser QueryParser)
    (org.apache.lucene.search IndexSearcher)
    (org.apache.lucene.store SimpleFSDirectory)
    (org.apache.lucene.analysis SimpleAnalyzer WhitespaceAnalyzer Token)
    (org.apache.lucene.analysis.tokenattributes OffsetAttribute TermAttribute)
    (org.apache.lucene.search BooleanQuery TermQuery BooleanClause$Occur)
    (org.apache.lucene.util Version))
  (:use [clojure.contrib.str-utils])
  (:require [clojure.string]))

(def version "0.2")

;
; Index management
;

(defn mk-index-writer
  "Constructs an index writer"
  [path analyzer]
  (let [dir (-> path File. SimpleFSDirectory.)]
    (IndexWriter. dir analyzer IndexWriter$MaxFieldLength/UNLIMITED)))

(defn close-index-writer
  "closes an index-writer"
  [idx-writer]
  (doto idx-writer (.commit) (.optimize) (.close)))

(defn mk-index-searcher
  "Constructs an index searcher"
  [path]
  (IndexSearcher. (IndexReader/open (-> path File. SimpleFSDirectory.))))

(defn close-index-searcher
  [idx-searcher]
  (. idx-searcher close))


;
; Document management
;
(defn- mk-field
  "constructs a field with keys
  :f :v :no-store? :no-index? :no-analyze?"
  [field]
  (Field. 
    (if (keyword? (:f field)) (name (:f field)) (str (:f field)))
    (str (if (nil? (:v field)) "" (:v field)))
    (if (:no-store field) Field$Store/NO Field$Store/YES)
    (if (:no-index field) Field$Index/NO 
      (if (:no-analyze field) Field$Index/NOT_ANALYZED Field$Index/ANALYZED))))

(defn mk-doc
  "Constructs a lucene docucment (mk-doc [{:keys (f v no-store no-index)}])"
  [& fields]
  (let [doc (Document.)]
    (do
      (doseq [field fields] (. doc add (mk-field field)))
      doc)))

;
; Indexing and querying
;
(defn mk-analyzer
  "Constructs an analyzer"
  [& args]
  (if (= :simple (nth args 1))
    (SimpleAnalyzer.)
    (WhitespaceAnalyzer.)))

(def *default-analyzer* (mk-analyzer))

(defn analyze-str
  "Apply an analyzer to a string"
  [analyzer string]
  (let [token-stream (. analyzer tokenStream nil (StringReader. string))
        termAttribute (.getAttribute token-stream TermAttribute)]
    (loop [result []]
      (if (.incrementToken token-stream)
        (recur (conj result (.term termAttribute)))
        result))))

(defn mk-query
  "constructs a query
   (mk-query [{:f field :v value :and yes :not yes}])"
  [& query-defs]
  (let [q (BooleanQuery.)]
    (do
      (doseq [query-def query-defs]
        (. q add 
          (TermQuery. (Term. (:f query-def) (:v query-def)))
          (if (:and query-def)
            (BooleanClause$Occur/MUST)
            (if (:not query-def)
              (BooleanClause$Occur/MUST_NOT)
              (BooleanClause$Occur/SHOULD)))))
      q)))

(defn mk-simple-query
  "constructs a query by simply performing the analysis and join the results
   into SHOULD clauses"
  ([flag analyzer field string]
   (let [q (BooleanQuery.)
        clause (if (= flag :and) (BooleanClause$Occur/MUST) (BooleanClause$Occur/SHOULD))]
    (do
      (doseq [token (analyze-str analyzer string)]
        (. q add (TermQuery. (Term. field token)) clause))
      q)))
  ([flag analyzer string]
    (mk-simple-query flag analyzer "__all__" string))
  ([analyzer string]
    (mk-simple-query :or analyzer string)))

(defn index-search [index-searcher query topk]
  (let [topdocs (. index-searcher search query topk)]
    (for [scoredoc (. topdocs scoreDocs)]
      (. index-searcher doc (. scoredoc doc)))))
